<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage core
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */


namespace mg;

include(__DIR__ . DS . 'Class.php');

/**
 * The \mg\Architecture is a class which maintains a consistent architectural view of the source code it is applied upon.
 * By scanning the given source folders and analyzing changed source files, {@link ClassUpdate \mg\ClassUpdates} are generated
 * which reflect architectural changes in the code.
 *
 * @author Michiel Hakvoort
 * @version 1.0
 * @see \mg\ClassSource
 * @see \mg\ClassUpdate
 */
class Architecture {

    /**
     * a simple map classname -> classobject
     *
     * @var \mg\Storage
     */
    private $classes = null;

    /**
     * a simple map classname -> boolean
     *
     * @var \mg\Storage
     */
    private $classList = null;

    /**
     * a simple map namespace -> boolean
     *
     * @var \mg\Storage
     */
    private $virtualList = null;

    // the set of class updates
    private $classUpdates = array();

    /**
     * A storage which contains 'classname' => array('classname' => distance') relation
     *
     * @var \mg\Storage
     */
    private $taxonomy = null;

    // simple unserialized cache
    private $classCache = array();

    private $logger = null;

    public function __construct(array $classSources) {
        $runtime = in(function(Runtime $runtime) { return $runtime; });

        $this->logger = in(function(Logger $logger) { return $logger; });

        $storageManager = in(function(StorageManager $storageManager) { return $storageManager; });

        $classes = $storageManager->getStorage('architecture.classes', true);
        $this->classes = $storageManager->cache($classes);

        $taxonomy = $storageManager->getStorage('architecture.taxonomy', true);
        $this->taxonomy = $storageManager->cache($taxonomy);

        $classList = $storageManager->getStorage('architecture.classlist', true);
        $this->classList = $storageManager->cache($classList);

        if($runtime->isTransactional()) {
            // cheap optimization
            include_once(__DIR__ . DS . 'PhpFileParser.php');

            $this->rebuild($classSources);
        }

    }

    private function rebuild($classSources) {
        /* @var $runtime Runtime */
        $runtime = in(function(Runtime $runtime) { return $runtime; });

        /* @var $fileSystem FileSystem */
        $fileSystem = in(function(FileSystem $fileSystem) { return $fileSystem; } );

        $oldAcceptedClasses = array();
        $oldAcceptedVirtuals = array();

        if(isset($this->classList['classlist'])) {
            $oldAcceptedClasses = unserialize($this->classList['classlist']);
        }

        if(isset($this->classList['virtuals'])) {
            $oldAcceptedVirtuals = unserialize($this->classList['virtuals']);
        }

        // perhaps create the implied differerence between transactional and deploying
        if($runtime->getMode() !== Runtime :: MODE_DEVELOPMENT) {
            // further optimize by clearing caches etc
        }

        $acceptedClasses = array();

        $updatedClasses = array();

        $acceptedVirtuals = array();

        // any encountered class which does not exist can and will be registered as a normal (virtual) class

        /* @var $classSource ClassSource */
        foreach($classSources as $classSource) {
            $classes = $classSource->getClasses();
            	
            $virtuals = $classSource->getVirtuals();

            foreach($virtuals as $virtual) {

                if(isset($acceptedVirtuals[$virtual])) {
                    throw new Exception("Duplicate virtual '{$virtual}'");
                }

                $acceptedVirtuals[$virtual] = true;

                if(isset($oldAcceptedVirtuals[$virtual])) {
                    unset($oldAcceptedVirtuals[$virtual]);
                }
            }

            // when is a class :

            // new     1) class with name wasn't known in previous iteration

            // removed 1) file is modified and class is not present
            //         2) file is removed and class is not seen in other file
            //         3) file is not marked as used by classes

            // modified 1)

            // moved 1) class is known in old file, class is removed from old file and class is present in new file
            //
            // conflicting? 1) there is more than one definition of a class (internal or external)

            /* @var $class ClassHeader */
            foreach($classes as $class) {
                $className = $class->getQualifiedName();

                if(isset($acceptedClasses[$className])) {
                    $acceptedClass = $acceptedClasses[$className];
                    $internalClass = $externalClass = null;

                    if($acceptedClass->isInternal()) {
                        $internalClass = $acceptedClass;
                        $externalClass = $class;
                    } elseif($class->isInternal()) {
                        $internalClass = $class;
                        $externalClass = $acceptedClass;
                    }

                    $locationMessage = $internalClass !== null
                    ? "class is internally defined and defined in '{$externalClass->getFile()}'"
                    : "class is defined in both '{$class->getFile()}' and '{$acceptedClass->getFile()}'";

                    throw new Exception("Duplicate class '{$className}', {$locationMessage}");
                }
                $acceptedClasses[$className] = $class;

                // Unknown classes are always added
                if(!isset($oldAcceptedClasses[$className])) {
                    $updatedClasses[$className] = $class;
                    continue;
                } else {
                    unset($oldAcceptedClasses[$className]);
                }

                // It is assumed that internal classes are never modified
                if($class->isInternal()) {
                    continue;
                }

                $fileStatus = $fileSystem->getFileStatus($class->getFile());

                // Classes from modified files are investigated for modifications
                if($fileStatus === FileSystem :: FILE_MODIFIED || $fileStatus === FileSystem :: FILE_ADDED) {
                    $updatedClasses[$className] = $class;
                }
            }

        }

        // Issue a remove update for removed classes (what about taxonomy?)
        foreach($oldAcceptedClasses as $className => $value) {
            $this->classUpdates[] = new ClassUpdate(unserialize($this->classes[$className]), null);
            unset($this->classes[$className]);
        }

        $oldAcceptedClasses = array();

        foreach($acceptedClasses as $className => $class) {
            // check for virtual?
            $oldAcceptedClasses[$className] = true;
        }

        // Update the currently known classes
        $this->classList['classlist'] = serialize($oldAcceptedClasses);


        foreach($updatedClasses as $class) {
            $className = $class->getQualifiedName();

            $previousClass = isset($this->classes[$className]) ? unserialize($this->classes[$className]) : null;

            $this->classes[$className] = serialize($class);

            $this->classUpdates[] = new ClassUpdate($previousClass, $class);
        }

        // Invalidate taxonomy paths for removed and "uninherited" classes

        /* @var $classUpdate ClassUpdate */
        foreach($this->classUpdates as $classUpdate) {
            // iff a class is updated, make sure the parents know whether the taxonomy still holds

            $className = $classUpdate->getQualifiedName();

            $parents = array();

            if($classUpdate->isRemoved()) {
                $classHeader = $classUpdate->getFrom();
                $parents = $classHeader->getParents();

                unset($this->taxonomy[$classHeader->getQualifiedName()]);
            } elseif(!$classUpdate->isAdded()) {
                $fromParents = $classUpdate->getFrom()->getParents();
                $toParents = $classUpdate->getTo()->getParents();

                $parents = array_diff($fromParents, $toParents);
            }

            reset($parents);
            while(count($parents) > 0) {
                $parent = array_shift($parents);

                if(!isset($this->taxonomy[$parent])) {
                    continue;
                }

                if(!isset($this->classes[$parent])) {
                    continue;
                }

                $parentClass = $this->getClass($parent);
                $updateClasses[$parent] = $parentClass;

                $parents = array_merge($parents, $parentClass->getParents());

                $taxonomy = unserialize($this->taxonomy[$parent]);

                // let all children do the math for the parent
                foreach($taxonomy as $childClassName => $distance) {
                    $class = $this->getClass($childClassName);

                    if($class !== null) {
                        $updatedClasses[$childClassName] = $class;
                    }
                }

                unset($this->taxonomy[$parent]);
            }

            // iff a class is removed, all parents should be updated and should figure out whether their taxonomy still holds


            // iff a class has a parent removed, the same condition holds for all parents
            // iff a class has a parent removed, notify that parent (and upward?)
        }

        // foreach updated class, update the taxonomy
        while(count($updatedClasses) > 0) {
            foreach($updatedClasses as $className => $class) {

                $parentClasses = $class->getParents();

                foreach($parentClasses as $parentClass) {
                    if(isset($updatedClasses[$parentClass])) {
                        continue 2;
                    }
                }

                if(!isset($this->taxonomy[$className])) {
                    $this->taxonomy[$className] = serialize(array($className => 0));
                }

                $taxonomy = unserialize($this->taxonomy[$className]);

                $parentDistance = 1;

                while(count($parentClasses) > 0) {
                    $newParentClasses = array();

                    foreach($parentClasses as $parentClass) {
                        if(!isset($this->taxonomy[$parentClass])) {
                            throw new Exception("Class '{$className}' inherits from non-existant '{$parentClass}'");
                        }

                        $parentTaxonomy = unserialize($this->taxonomy[$parentClass]);

                        $propagate = false;

                        foreach($taxonomy as $class => $distance) {
                            $distance += $parentDistance;

                            if(!isset($parentTaxonomy[$class]) || $parentTaxonomy[$class] > $distance) {
                                $propagate = true;
                                $parentTaxonomy[$class] = $distance;
                            }
                        }

                        if($propagate) {
                            $this->taxonomy[$parentClass] = serialize($parentTaxonomy);

                            $parentClassHeader = $this->getClass($parentClass);
                            $newParentClasses = array_merge($newParentClasses, $parentClassHeader->getParents());
                        }
                    }

                    $parentClasses = array_unique($newParentClasses);

                    $parentDistance++;

                }

                // class is safe to update taxonomy
                unset($updatedClasses[$className]);
            }
        }

    }

    /**
     * Get the taxonomy for the class with given name
     * @param $className The taxonomy
     * @return array
     */
    public function getTaxonomy($className) {
        $className = mb_strtolower($className);
        if(isset($this->taxonomy[$className])) {
            return unserialize($this->taxonomy[$className]);
        }

        return null;
    }
    /**
     * Get the {@link \mg\ClassHeader} with the given name. In order for a class to be returned, the class must be defined
     * in any of the {@link \mg\ClassSource ClassSources} used to construct the \mg\Architecture.
     *
     * @access public
     * @param string $name
     * @return \mg\ClassHeader The {@link \mg\ClassHeader} with the given name, or null if the class is not specified
     */
    public function getClass($name) {
        $name = mb_strtolower($name);

        if(isset($this->classCache[$name])) {
            return $this->classCache[$name];
        }

        if(!isset($this->classes[$name])) {
            return null;
        }

        $result = unserialize($this->classes[$name]);
        $this->classCache[$name] = $result;

        return $result;
    }

    /**
     * Get the {@link \mg\ClassUpdate ClassUpdates} which have occured since the previous iteration.
     *
     * @access public
     * @return array The set of {@link \mg\ClassUpdate ClassUpdates}
     */
    public function getUpdates() {
        return $this->classUpdates;
    }

    /**
     * Return the smallest inheritance distance between two classes (or interfaces).
     *
     * @access public
     * @param string $subClass The name of the subclass
     * @param string $parentClass The name of the parent class
     * @return int|boolean Returns false when subClass does not extend nor implement parentClass
     */
    public function getClassDistance($subClass, $parentClass) {
        $subClass = mb_strtolower($subClass);
        $parentClass = mb_strtolower($parentClass);

        if(!isset($this->taxonomy[$parentClass])) {
            return false;
        }

        if($subClass === $parentClass) {
            return 0;
        }

        $subClasses = unserialize($this->taxonomy[$parentClass]);

        if(!isset($subClasses[$subClass])) {
            return false;
        }

        return $subClasses[$subClass];

    }

    /**
     * Get all {@link \mg\ClassHeader ClassHeaders} found in the {@link \mg\ClassSource ClassSources}.
     *
     * @access public
     * @return array The set of all {@link \mg\ClassHeader ClassHeaders} found in the {@link \mg\ClassSource ClassSources}.
     */
    public function getClasses() {
        $acceptedClasses = unserialize($this->classList['classlist']);
        $result = array();
        foreach($acceptedClasses as $name => $bool) {
            $result[$name] = unserialize($this->classes[$name]);
        }
        return $result;
    }

}

/**
 * A \mg\ClassUpdate denotes an architectural change. This architectural change can
 * mean that a {@link \mg\ClassHeader} was added, a \mg\ClassHeader was removed, a \mg\ClassHeader was
 * located to another file, or that the body of a \mg\ClassHeader was changed.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 * @see \mg\ClassHeader
 * @see \mg\Architecture
 */
class ClassUpdate {

    /**
     * @var \mg\ClassHeader
     */
    private $oldClass;

    /**
     * @var \mg\ClassHeader
     */
    private $newClass;
    private $qualifiedName;

    /**
     * Create a new \mg\ClassUpdate with two {@link \mg\ClassHeader ClassHeaders} as source and target class
     *
     * @access public
     * @param \mg\ClassHeader $oldClass The {@link \mg\ClassHeader} the new \mg\ClassHeader evolves from, when specified as null the new \mg\ClassHeader is added
     * @param \mg\ClassHeader $newClass The {@link \mg\ClassHeader} the old \mg\ClassHeader evolves into, when specified as null the old \mg\ClassHeader is removed
     */
    public function __construct(ClassHeader $oldClass = null, ClassHeader $newClass = null) {
        $this->oldClass = $oldClass;
        $this->newClass = $newClass;
        if($oldClass === null && $newClass === null) {
            throw new Exception('At least one class needs to be specified!');
        }
        $this->qualifiedName = ($oldClass === null) ? $newClass->getQualifiedName() : $oldClass->getQualifiedName();
    }

    /**
     * Get the name of the {@link \mg\ClassHeader} which is covered by the \mg\ClassUpdate.
     *
     * @access public
     * @return string The name of the {@link \mg\ClassHeader}
     */
    public function getQualifiedName() {
        return $this->qualifiedName;
    }

    public function getNamespace() {
        if(($index = mb_strrpos($this->qualifiedName, '\\')) !== false) {
            return mb_substr($this->qualifiedName, 0, $index);
        }

        return '';
    }
    /**
     * Check whether the \mg\ClassUpdate denotes an added {@link \mg\ClassHeader}.
     *
     * @access public
     * @return boolean True when the \mg\ClassUpdate denotes an added {@link \mg\ClassHeader}
     */
    public function isAdded() {
        return $this->oldClass === null;
    }

    /**
     * Check whether the \mg\ClassUpdate denotes a removed {@link \mg\ClassHeader}.
     *
     * @access public
     * @return boolean True when the \mg\ClassUpdate denotes a removed {@link \mg\ClassHeader}
     */
    public function isRemoved() {
        return $this->newClass === null;
    }

    /**
     * Get the {@link \mg\ClassHeader} the \mg\ClassUpdate evolves from.
     *
     * @access public
     * @return \mg\ClassHeader The {@link \mg\ClassHeader} the \mg\ClassUpdate evolves from
     */
    public function getFrom() {
        return $this->oldClass;
    }

    /**
     * Get the {@link \mg\ClassHeader} the \mg\ClassUpdate evolves into.
     *
     * @access public
     * @return \mg\ClassHeader The {@link \mg\ClassHeader} the \mg\ClassUpdate evolves into
     */
    public function getTo() {
        return $this->newClass;
    }

    /**
     * Check whether the \mg\ClassUpdate denotes a modified {@link \mg\ClassHeader}.
     *
     * @access public
     * @return boolean True when the \mg\ClassUpdate denotes a modified {@link \mg\ClassHeader}
     */
    public function isSignatureChanged() {
        if($this->isAdded() || $this->isRemoved()) {
            return false;
        }

        return (count(array_diff($this->oldClass->getParents(), $this->newClass->getParents())) > 0)
        || (count(array_diff($this->newClass->getParents(), $this->oldClass->getParents())) > 0);
    }

    /**
     * Check whether the \mg\ClassUpdate denotes a moved {@link \mg\ClassHeader}.
     *
     * @access public
     * @return boolean True when the \mg\ClassUpdate denotes a moved {@link \mg\ClassHeader}
     */
    public function isMoved() {
        if($this->isAdded() || $this->isRemoved()) {
            return false;
        }

        return $this->oldClass->getFile() !== $this->newClass->getFile();
    }

    /**
     * {@inheritDoc}
     */
    public function __toString() {
        $result = $this->getName() . ' : ';
        if($this->isAdded()) {
            $result .= 'ADDED';
        } elseif($this->isRemoved()) {
            $result .= 'REMOVED';
        } else {
            if($this->isMoved()) {
                $result .= 'MOVED';
            }
            if($this->isSignatureChanged()) {
                if($this->isMoved()) {
                    $result .= ', ';
                }
                $result .= 'CHANGED';
            }
        }
        return $result;
    }
}
